﻿using System;
using System.Collections.Generic;
using System.IO;
using Ionic.Zip;
using System.Xml;
using System.Text;

namespace EpubTools
{
	public class NookTools
	{
		/// <summary>
		/// Rewrite the &lt;meta name="cover" /&gt; tag in an ePub OPF file to conform to
		/// the attribute order a Nook expects (name then value).
		/// </summary>
		/// <param name="fileName"></param>
		public static void updateCoverMetadata(string fileName)
		{
			bool returnVal = false;
			updateCoverMetadata(fileName, out returnVal);
		}

		/// <summary>
		/// Rewrite the &lt;meta name="cover" /&gt; tag in an ePub OPF file to conform to
		/// the attribute order a Nook expects (name then value).
		/// </summary>
		/// <param name="fileName"></param>
		public static void updateCoverMetadata(string fileName, out bool isAnyUpdates)
		{
			List<String> errors = new List<String>();
			bool preserveWhitespace = false;
			isAnyUpdates = false;

			// TODO: This fails utterly if the container.xml or OPF is XML 1.1 and has a byte-order mark, or is a multibyte encoding.

			try
			{
				using (ZipFile zip = ZipFile.Read(fileName))
				{
					foreach (ZipEntry zeContainer in zip.SelectEntries("container.xml", "META-INF"))
					{
						// Unzip the container.xml file.
						var streamContainer = new MemoryStream();
						zeContainer.Extract(streamContainer);
						streamContainer.Seek(0, SeekOrigin.Begin);

						// HACK: Convert from XML 1.1 to XML 1.0 if necessary, since Microsoft's XML tools only support 1.0.
						var updatedContainer = convertXml11ToXml10(streamContainer);
						if (updatedContainer)
						{
							System.Diagnostics.Debug.WriteLine("Updated container " + zip.Name);
						}

						// Open the container.xml (or the de-XML 1.1'd container.xml) for parsing.
						XmlDocument xmlContainer = new XmlDocument();
						xmlContainer.PreserveWhitespace = preserveWhitespace;
						xmlContainer.Load(streamContainer);

						string opfPath = "";
						string opfRoot = "";
						string opfFile = "";

						/*
							* Parse the container.xml looking for the OPF file:
							* <container>
							*   <rootfiles>
							*     <rootfile media-type="application/oebps-package+xml" full-path="WHAT WE ARE PARSING FOR" />
							*   </rootfiles>
							* </container>
							*/

						// Advance to the <container>.
						XmlNode container = xmlContainer.FirstChild;
						while (container.Name != "container")
						{
							container = container.NextSibling;
						}

						// Find the <rootfiles> in the <container>.
						XmlNode rootfiles = container.FirstChild;
						while (rootfiles.Name != "rootfiles")
						{
							rootfiles = rootfiles.NextSibling;
						}

						// Parse the <rootfiles> for our specific <rootfile>, based on media-type.
						XmlNode rootfile = rootfiles.FirstChild;
						while ((rootfile != null) && (opfPath == ""))
						{
							XmlAttributeCollection attributes = rootfile.Attributes;
							XmlAttribute mediaType = attributes["media-type"];
							if ((attributes["media-type"].Value).Equals("application/oebps-package+xml"))
							{
								opfPath = attributes["full-path"].Value;
								int indexSlash = opfPath.IndexOf('/');
								int temp = indexSlash;
								while (temp > -1)
								{
									indexSlash = temp;
									temp = opfPath.IndexOf('/', temp + 1);
								}
								opfRoot = (indexSlash > -1) ? opfPath.Substring(0, indexSlash + 1) : "";
								opfFile = (indexSlash > -1) ? opfPath.Substring(indexSlash + 1) : opfPath;
							}
							rootfile = rootfile.NextSibling;
						}

						// Theoretically at this point we know where the OPF file is. Let's open 'er up!
						foreach (ZipEntry zeOpf in zip.SelectEntries(opfFile, opfRoot))
						{
							var streamOpf = new MemoryStream();
							zeOpf.Extract(streamOpf);
							streamOpf.Seek(0, SeekOrigin.Begin);

							// HACK: Convert from XML 1.1 to XML 1.0 if necessary, since Microsoft's XML tools only support 1.0.
							var updatedOpf = convertXml11ToXml10(streamOpf);
							if (updatedOpf)
							{
								System.Diagnostics.Debug.WriteLine("Updated OPF " + zip.Name);
							}

							XmlDocument opf = new XmlDocument();
							opf.PreserveWhitespace = preserveWhitespace;
							try
							{
								bool foundCover = false;

								opf.Load(streamOpf);

								// Advance to the package node.
								XmlNode package = opf.FirstChild;
								while (package.LocalName != "package")
								{
									package = package.NextSibling;
								}

								// Find the metadata node.
								XmlNode metadata = package.FirstChild;
								while (metadata.LocalName != "metadata")
								{
									metadata = metadata.NextSibling;
								}

								// Find the meta tags, if any.
								XmlNode metaCover = metadata.FirstChild;
								while (metaCover != null)
								{
									if (metaCover.Name == "meta")
									{
										XmlAttribute coverNameNode = metaCover.Attributes["name"];
										if (coverNameNode.Value == "cover")
										{
											foundCover = true;
											bool updatedAttributes = false;
											XmlAttribute insertAfterAttribute = coverNameNode;

											while (metaCover.Attributes[0] != coverNameNode)
											{
												XmlAttribute coverOtherAttribute = metaCover.Attributes[0];
												metaCover.Attributes.Remove(coverOtherAttribute);
												metaCover.Attributes.InsertAfter(coverOtherAttribute, insertAfterAttribute);
												insertAfterAttribute = coverOtherAttribute;

												updatedAttributes = true;
											}

											if (updatedAttributes)
											{
												// Save the XML to a stream, and prep the stream for reading.
												var opfStream = new MemoryStream();
												opf.Save(opfStream);
												opfStream.Seek(0, SeekOrigin.Begin);

												// Write the stream to the ZIP file.
												zip.UpdateEntry(opfPath, opfStream);
												zip.Save();
												isAnyUpdates = true;
											}
										}
									}
									metaCover = metaCover.NextSibling;
								}

								if (!foundCover)
								{
									// Didn't find the cover, need to look in the OPF for a Mobipocket cover image.
									XmlNode guide = package.FirstChild;
									while ((guide != null) && (guide.Name != "guide"))
									{
										guide = guide.NextSibling;
									}

									// Find the reference.
									if (guide != null)
									{
										XmlNode reference = guide.FirstChild;
										while ((reference != null) && !foundCover)
										{
											if (reference.Attributes["type"].Value == "other.ms-coverimage-standard")
											{
												// HACK: Okay, we know the HREF for our cover. Now we need to find the matching ID. And we're
												// going to be dicks and make some assumptions - namely, that if the reference *ends* with the
												// name of our file, it's valid.
												XmlNode manifest = package.FirstChild;
												while (manifest.Name != "manifest")
												{
													manifest = manifest.NextSibling;
												}

												XmlNode item = manifest.FirstChild;
												while ((item != null) && !foundCover)
												{
													if (item.Attributes["href"].Value.EndsWith(reference.Attributes["href"].Value))
													{
														XmlElement newCover = opf.CreateElement("meta");
														newCover.SetAttribute("name", "cover");
														newCover.SetAttribute("content", item.Attributes["id"].Value);
														metadata.AppendChild(newCover);

														// Save the XML to a stream, and prep the stream for reading.
														var opfStream = new MemoryStream();
														opf.Save(opfStream);
														opfStream.Seek(0, SeekOrigin.Begin);

														// Write the stream to the ZIP file.
														zip.UpdateEntry(opfPath, opfStream);
														zip.Save();
														isAnyUpdates = true;

														foundCover = true;
													}
													item = item.NextSibling;
												}
											}
											reference = reference.NextSibling;
										}
									}
								}
							}
							catch (XmlException e)
							{
								System.Console.Out.WriteLine(e);
								errors.Add(e.ToString());
							}
						}
					}
				}
			}
			catch (Exception)
			{
				isAnyUpdates = false;
			}
		}

		/// <summary>
		/// "Convert" the specified XML stream to an XML 1.0 stream by
		/// updating the XML header, if needed (This does no validation
		/// or conversion of other XML 1.0-invalid semantics.)
		/// </summary>
		/// <param name="xmlStream">Stream to convert if necessary</param>
		private static bool convertXml11ToXml10(Stream xmlStream)
		{
			if (!xmlStream.CanRead)
			{
				throw new ArgumentException("Stream does not support reading", "xmlStream");
			}
			else if (!xmlStream.CanWrite)
			{
				throw new ArgumentException("Stream does not support writing", "xmlStream");
			}
			else if (!xmlStream.CanSeek)
			{
				throw new ArgumentException("Stream does not support seeking", "xmlStream");
			}

			// Save our current position, and reset to the start of the file.
			// (Our other code should be "good" and reset to the start of the
			// file already, this is for saftey's sake.)
			bool updatedStream = false;
			var initPosition = xmlStream.Position;
			xmlStream.Seek(0, SeekOrigin.Begin);

			// The XML declaration, including version, is specced as being
			// the first 19 characters of the file. So we're going to cheat.
			var xmlReader = new StreamReader(xmlStream);
			var declaration = xmlReader.ReadLine();
			if (declaration.Substring(0, 19) == "<?xml version=\"1.1\"")
			{
				// Rewrite the line.
				xmlStream.Seek(0, SeekOrigin.Begin);
				var lineBuilder = new StringBuilder();
				lineBuilder.Append("<?xml version=\"1.0\"");
				lineBuilder.Append(declaration.Substring(19));
				var xmlWriter = new StreamWriter(xmlStream);
				xmlWriter.WriteLine(lineBuilder.ToString());
				xmlWriter.Flush();

				updatedStream = true;
			}

			xmlStream.Seek(initPosition, SeekOrigin.Begin);
			return updatedStream;
		}
	}
}
